package com.weiss.jpro.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.weiss.jpro.dto.Result;
import com.weiss.jpro.entity.Shop;
import com.weiss.jpro.mapper.ShopMapper;
import com.weiss.jpro.service.IShopService;
import com.weiss.jpro.utils.CacheClient;
import com.weiss.jpro.utils.RedisConstants;
import com.weiss.jpro.utils.RedisData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import javax.management.RuntimeErrorException;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.weiss.jpro.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.weiss.jpro.utils.RedisConstants.CACHE_SHOP_TTL;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Slf4j
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    // 注入创建的工具类
    @Resource
    private CacheClient cacheClient;

    @Override
    public Result queryById(Long id) {
//        // 1.从redis查询商户缓存
//        String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
//        String shopJson = stringRedisTemplate.opsForValue().get(shopKey);
//
//        // 2.判断redis是否命中
//        if (StrUtil.isNotBlank(shopJson)) {
//            // 存在，直接返回
//            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
//            log.debug("redis命中");
//            return Result.ok(shop);
//        }
//
//        // 2.5 判断命中的是否是空字符串，前面已经判断有值，只剩下null和""，不是null就是空字符串
//        // 上面2.通过isNotBlank判断是否有值，无值还存在null和""的情况
//        if (shopJson != null) {
//            // redis命中之前存的空字符串
//            // 避免大量使用不存在店铺信息访问数据库
//            log.debug("请不求在id，redis命中空字符串");
//            return Result.fail("店铺不存在");
//        }
//
//        // 3.不存在，根据id从数据库查询
//        Shop shop = getById(id);
//        log.debug("redis未命中");
//
//        // 缓存穿透问题：
//        // 缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在
//        // 这样缓存永远不会生效，请求都会打到数据库
//        // 常见解决方案
//        // 缓存空对象：实现简单，维护方便
//        // 缺点：额外的内存消耗，可能造成短期的不一致(设置TTL过期时间)
//        // 布隆过滤：内存占用少，没有多余的key
//        // 缺点：实现复杂，存在误判可能
//
//        // 上面都是被动解决，还可以主动减少被穿透的几率
//        // 增强id复杂度，避免被猜测id规律
//        // 做好数据的基础格式校验，不符合的直接拦截，如登录里面的手机号格式校验
//        // 加强用户权限校验，如要求先登录，登录之后还可以进行限流，对访问频率等进行设置
//        // 做好热点参数的限流
//
//        // 缓存雪崩问题：
//        // 缓存雪崩是指在同一时间段大量的缓存key同时失效或者redis服务宕机，
//        // 导致大量请求达到数据库，带来巨大的压力
//        // 解决方案：
//        // 1.给不同key的TTL添加随机值，如TTL为30分钟，在后面加上
//        // 一个0-5的随机值，失效时间就随机在30-35之间，缓解大量key同时失效
//        // 2.利用redis集群提高服务的可用性，避免宕机
//        // 3.给缓存业务添加降级限流策略，如缓存雪崩后直接拒绝处理请求
//        // 4.给业务添加多级缓存，浏览器本地缓存->nginx缓存->redis缓存->JVM缓存，多级缓存缓解雪崩
//
//        // 缓存击穿问题：
//        // 缓存击穿问题也叫热点key问题，就是一个被高并发访问且
//        // 缓存重建业务较复杂的key突然失效了，无数请求访问会在
//        // 瞬间给数据库带来巨大的冲击
//        // 例：线程1查询缓存，未命中，去数据库查询然后重建缓存，
//        // 在缓存重建未完成之前，线程2查询未命中，也去数据库查询然后重建缓存，
//        // 线程n也如此，同时对数据库进行查询造成巨大访问压力
//        // 常见解决方案：
//        // 互斥锁
//        // 例：线程1查询缓存，未命中，获取互斥锁，然后查询数据库重建缓存
//        // 释放锁。缓存重建完成之前线程2也来查询，未命中，获取互斥锁失败，
//        // 休眠一段时间再来重试，此时若线程1重建缓存完成，直接命中
//        // 若线程1构建比较久，但是大量线程重试等待，会导致服务性能下降，所以有了逻辑过期
//        // 逻辑过期：通过降低一致性，避免性能下降
//        // 缓存时不直接expire设置ttl，而是在缓存中添加expire字段，设置逻辑过期时间
//        // 例：线程1查询缓存，发现逻辑时间已经过期，是旧数据，需要更新
//        // 线程1获取互斥锁，然后开启新线程线程2，让线程2查询数据库重建缓存，重置逻辑过期时间，然后释放锁
//        // 在线程1开启新线程后，自己则返回旧缓存数据
//        // 若线程3在互斥锁存在期间访问缓存，发现逻辑过期，尝试获取互斥锁，
//        // 发现获取失败，则不用等待重试，直接返回旧缓存数据
//        // 两种方案对比：
//        // 互斥锁：没有额外的内存消耗，保证一致性，实现简单
//        // 缺点是线程需要等待，性能受影响；
//        // 可能有死锁风险：假设一个业务有对多个缓存查询的需求，另一个业务也有，
//        // 此时第一个业务拿到一把锁，要获取另一个缓存的锁时，发现锁在另一个业务里，就会产生相互等待这种死锁的情况
//        // 逻辑过期：线程无需等待，性能较好
//        // 缺点是不保证一致性，有额外内存消耗，实现复杂
//
//        // 4.不存在返回错误
//        if (shop == null) {
//            // 4.5 将空字符串写入redis，存储不存在的店铺信息缓存""，设置TTL为2分钟->回到2.5编写对应处理逻辑
//            stringRedisTemplate.opsForValue().set(shopKey, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
//            log.debug("使用不存在id进行访问，存储空字符串到redis");
//            return Result.fail("店铺不存在");
//        }
//
//        // 5.存在，写入redis并返回数据
//        // 缓存更新策略，先写数据库再删除缓存；设置超时剔除
//        String Json = JSONUtil.toJsonStr(shop);
//        stringRedisTemplate.opsForValue().set(shopKey, Json, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES); // 超时剔除
// ---------------------------------------------------------------------------------------------------------------------------------
        // 缓存穿透
//        Shop shop = queryWithPassThrough(id);

        // 使用工具类解决缓存穿透
        // lambda表达式简写，id2 -> getById(id2)可以简写为this::getById
//        Shop shop = cacheClient
//                .queryWithPassThrough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

        // 互斥锁解决缓存击穿
//        Shop shop = queryWithNutex(id);

        // 用逻辑过期解决缓存击穿
        // 活动相关，已经提前进行数据预热，默认都会命中，不用考虑缓存穿透
//        Shop shop = queryWithLogicExpire(id);

        // 使用工具类解决缓存穿透
        Shop shop = cacheClient.queryWithLogicExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.SECONDS);

        if (shop == null) {
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
    }

    // 创建线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    // 将通逻辑过期相关代码进行封装
    public Shop queryWithLogicExpire(Long id) {
        // 1.从redis查询商户缓存
        String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(shopKey);

        // 2.判断redis是否命中
        if (StrUtil.isBlank(shopJson)) {
            // 未命中，直接返回
            log.debug("redis未命中，不是热点key");
            return null;
        }

        // 3命中，判断过期时间
        // 反序列化json
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        // 不知道是什么类型会转成JSONObject, RedisData里面是Object
        JSONObject data = (JSONObject) redisData.getData();
        // toBean还可以接收JSONObject
        Shop shop = JSONUtil.toBean(data, Shop.class);
        // 获取过期时间
        LocalDateTime expireTime = redisData.getExpireTime();
        // 判断是否过期 // 过期时间是否在当前时间之后
        if (expireTime.isAfter(LocalDateTime.now())) {
            //3.1 未过期，返回店铺信息
            log.debug("未过期，返回店铺信息");
            return shop;
        }

        //3.2 已过期，需要缓存重建
        //4.缓存重建
        //4.1 获取互斥锁
        String lockKey = "lock:shop:" + id;
        boolean isLock = tryLock(lockKey);
        // 注意：获取锁成功应该再次检测redis缓存是否过期，做doublecheck，如果存在则无需重建缓存
        // 避免缓存重建释放锁的瞬间另一个线程拿到锁，再次重建缓存
        // 例：测试时将并发数量调到2000以上就会重建缓存多次
        // 这里的锁不是线程锁，是redis的锁，所有线程都可以删除，所以可以新建线程释放锁
        // 这里加锁是为了只有一个线程开启子线程去重建缓存
        if (isLock) {
            //4.2 成功，开启独立线程，实现缓存重建
            // 通过线程池来进行，避免经常创建和销毁
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 重建缓存
                    log.debug("热点key过期，进行缓存重建");
                    this.saveShop2Redis(id, 20L); // 正常应该设置30分钟，这里设置20秒方便过期，进行测试
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    // 释放锁
                    unLock(lockKey);
                }
            });
        }

        //4.3 失败还是失败，都会返回过期信息
        log.debug("返回过期信息");
        return shop;
    }

    // 提前加入热点key进行数据预热，通过单元测试添加，下面为封装的添加方法
    public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
        // 1.查询店铺数据
        Shop shop = getById(id);
        // 模拟重建缓存延迟
        Thread.sleep(200);

        // 2.添加逻辑过期时间，封装成RedisData
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds)); // 在当前时间基础上加上多少秒，代表逻辑过期时间

        // 3.写入redis
        String redisJsonData = JSONUtil.toJsonStr(redisData);
        stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, redisJsonData);
    }

    // 将缓存击穿相关代码进行封装
    public Shop queryWithNutex(Long id) {
        // 1.从redis查询商户缓存
        String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(shopKey);

        // 2.判断redis是否命中
        if (StrUtil.isNotBlank(shopJson)) {
            // 存在，直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            log.debug("redis命中");
            return shop;
        }

        // 2.5 判断命中的是否是空字符串，前面已经判断有值，只剩下null和""，不是null就是空字符串
        // 上面2.通过isNotBlank判断是否有值，无值还存在null和""的情况
        if (shopJson != null) {
            // redis命中之前存的空字符串
            // 避免大量使用不存在店铺信息访问数据库
            log.debug("请不求在id，redis命中空字符串");
            return null;
        }

        // 4.实现缓存重建
        // 4.1 获取互斥锁
        String lockKey = "lock:shop:" + id;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2 判断获取是否成功
            if (!isLock) {
                // 4.3 失败，休眠并重试
                Thread.sleep(50); // 休眠50ms
                return queryWithNutex(id);
            }

            // 4.4 成功，根据id查询数据库
            shop = getById(id);
            // 认为创造并发，模拟重建延时
            Thread.sleep(200);
            log.debug("redis未命中");
            if (shop == null) {
                // 4.5 将空字符串写入redis，存储不存在的店铺信息缓存""，设置TTL为2分钟->回到2.5编写对应处理逻辑
                stringRedisTemplate.opsForValue().set(shopKey, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                log.debug("使用不存在id进行访问，存储空字符串到redis");
                return null;
            }

            // 5.存在，写入redis并返回数据
            // 缓存更新策略，先写数据库再删除缓存；设置超时剔除
            String Json = JSONUtil.toJsonStr(shop);
            stringRedisTemplate.opsForValue().set(shopKey, Json, CACHE_SHOP_TTL, TimeUnit.MINUTES); // 超时剔除

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 6.释放互斥锁
            unLock(lockKey);
        }

        return shop;
    }

    // 将缓存穿透相关代码进行封装
    public Shop queryWithPassThrough(Long id) {
        // 1.从redis查询商户缓存
        String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(shopKey);

        // 2.判断redis是否命中
        if (StrUtil.isNotBlank(shopJson)) {
            // 存在，直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            log.debug("redis命中");
            return shop;
        }

        // 2.5 判断命中的是否是空字符串，前面已经判断有值，只剩下null和""，不是null就是空字符串
        // 上面2.通过isNotBlank判断是否有值，无值还存在null和""的情况
        if (shopJson != null) {
            // redis命中之前存的空字符串
            // 避免大量使用不存在店铺信息访问数据库
            log.debug("请不求在id，redis命中空字符串");
            return null;
        }

        // 3.不存在，根据id从数据库查询
        Shop shop = getById(id);
        log.debug("redis未命中");

        // 4.不存在返回错误
        if (shop == null) {
            // 4.5 将空字符串写入redis，存储不存在的店铺信息缓存""，设置TTL为2分钟->回到2.5编写对应处理逻辑
            stringRedisTemplate.opsForValue().set(shopKey, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            log.debug("使用不存在id进行访问，存储空字符串到redis");
            return null;
        }

        // 5.存在，写入redis并返回数据
        // 缓存更新策略，先写数据库再删除缓存；设置超时剔除
        String Json = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(shopKey, Json, CACHE_SHOP_TTL, TimeUnit.MINUTES); // 超时剔除
        return shop;
    }

    // 获取锁
    private boolean tryLock(String lock) {
        // 利用setnx是实现互斥锁，获取锁setnx lock ex 10s, 释放锁del lock
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lock, "1", 10, TimeUnit.SECONDS);
        // 需要把包装类型转成基本类型返回，直接返回自动拆箱有可能存在空指针，使用hutool工具类手动拆箱
        return BooleanUtil.isTrue(flag);
    }
    // 释放锁
    private void unLock(String lock) {
        stringRedisTemplate.delete(lock);
    }

    @Override
    @Transactional // 数据库更新和redis缓存删除为一个事务，保证原子性
    public Result updateShop(Shop shop) {
        // 1.更新数据库
        updateById(shop);
        // 2.删除缓存
        // 获取id，判断是否合规
        Long shopId = shop.getId();
        if (shopId == null) {
            return Result.fail("店铺id不能为空");
        }
        String shopKey = RedisConstants.CACHE_SHOP_KEY + shopId;
        stringRedisTemplate.delete(shopKey);
        return Result.ok();
    }
}